﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using MrDev.GameObjects;
using MrDev.Graphics;
using MrDev.GameMath;

namespace MrDev.Physics
{
    class ObjectPhysics
    {
        public event Level.BossScreenDelegate EnterBossScreen;
        public event Level.BossScreenDelegate ExitBossScreen;

        /// <summary>
        /// Updates the player's velocity and position based on input, gravity, etc.
        /// </summary>
        public void ApplyPhysics(GameObject sourceObject, GameTime gameTime)
        {
            PhysicsParameters sourceParameters = sourceObject.Parameters;
            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

            sourceParameters.PreviousBounds = AbsoluteCollisionRect(sourceObject);
            sourceParameters.PreviousPosition = sourceParameters.Position;
            sourceParameters.Velocity.X += sourceParameters.Movement * sourceParameters.MoveAcceleration * elapsed;
            if (sourceParameters.IsOnGround)
                sourceParameters.Velocity.X *= sourceParameters.GroundDragFactor;
            else
                sourceParameters.Velocity.X *= sourceParameters.AirDragFactor;


            // Handle stuff when we stand on an alien object
            if (sourceObject.Alien != null)
            {
                PhysicsParameters alienParameters = sourceObject.Alien.Parameters;
                Vector2 alienTranslate = alienParameters.Position - alienParameters.PreviousPosition;
                // Add movement from alien and opposite gravityforce from alien
                sourceParameters.Position += alienTranslate;
            }

            if (sourceObject.Alien == null && !sourceParameters.IsOnGround)
            {
                // Apply gravity
                sourceParameters.Velocity.Y += sourceParameters.GravityAcceleration * elapsed;
            }


            // Clamp velocity to avoid insane speed.
            sourceParameters.Velocity.X = MathHelper.Clamp(sourceParameters.Velocity.X, -sourceParameters.MaxMoveSpeed, sourceParameters.MaxMoveSpeed);
            sourceParameters.Velocity.Y = MathHelper.Clamp(sourceParameters.Velocity.Y, -sourceParameters.MaxMoveSpeed, sourceParameters.MaxMoveSpeed);

            // Apply velocity to position
            sourceParameters.Position += sourceParameters.Velocity * elapsed;
        }

        public Rectangle AbsoluteCollisionRect(GameObject sourceObject)
        {
            PhysicsParameters objectParameters = sourceObject.Parameters;
            int left = (int)Math.Round(objectParameters.Position.X - objectParameters.SpriteBottomCenter.X);
            int top = (int)Math.Round(objectParameters.Position.Y - objectParameters.SpriteBottomCenter.Y);
            int width;
            int height;
            if (objectParameters.RelativeCollisionRect.HasValue)
            {
                left += objectParameters.RelativeCollisionRect.Value.X;
                top += objectParameters.RelativeCollisionRect.Value.Y;
                width = objectParameters.RelativeCollisionRect.Value.Width;
                height = objectParameters.RelativeCollisionRect.Value.Height;
            }
            else
            {
                width = objectParameters.SpriteSize.Width;
                height = objectParameters.SpriteSize.Height;
            }

            return new Rectangle(left, top, width, height);
        }

        /// <summary>
        /// Detects and resolves all collisions between the player and his neighboring
        /// tiles. When a collision is detected, the player is pushed away along one
        /// axis to prevent overlapping. There is some special logic for the Y axis to
        /// handle platforms which behave differently depending on direction of movement.
        /// </summary>
        public bool HandleCollisions(GameObject sourceObject)
        {
            Level level = sourceObject.Level;
            ObjectPhysics physics = level.Physics;
            PhysicsParameters objectParameters = sourceObject.Parameters;

            bool objectCollided = false;
            Rectangle sourceObjectCollisionRect = AbsoluteCollisionRect(sourceObject);
            // Abandon alien object if we are outside the alien bounds...
            if (sourceObject.Alien != null)
            {
                Rectangle alientObjectCollisionRect = physics.AbsoluteCollisionRect(sourceObject.Alien);
                if (sourceObjectCollisionRect.Right < alientObjectCollisionRect.Left ||
                    sourceObjectCollisionRect.Left > alientObjectCollisionRect.Right)
                {
                    DetachObject(sourceObject);
                }
            }

            // Get the player's bounding rectangle and find neighboring tiles.
            Rectangle bounds = sourceObjectCollisionRect;
            int leftTile = (int)Math.Floor((float)bounds.Left / Tile.Width);
            int rightTile = (int)Math.Ceiling(((float)bounds.Right / Tile.Width)) - 1;
            int topTile = (int)Math.Floor((float)bounds.Top / Tile.Height);
            int bottomTile = (int)Math.Ceiling(((float)bounds.Bottom / Tile.Height)) - 1;
            objectCollided = false;

            // For each potentially colliding tile,
            for (int y = topTile; y <= bottomTile; ++y)
            {
                for (int x = leftTile; x <= rightTile; ++x)
                {
                    // If this tile is collidable,
                    TileCollision collision = level.GetCollision(x, y);
                    if (collision == TileCollision.EnterBossScreen && EnterBossScreen!=null)
                    {
                        EnterBossScreen(sourceObject, x, y);
                    }
                    else if (collision == TileCollision.ExitBossScreen && EnterBossScreen != null)
                    {
                        ExitBossScreen(sourceObject, x, y);
                    }
                    else if (collision == TileCollision.Impassable)
                    {
                        // Determine collision depth (with direction) and magnitude.
                        Rectangle tileBounds = level.GetTileBounds(x, y);
                        Vector2 depth = RectangleExtensions.GetIntersectionDepth(bounds, tileBounds);
                        if (depth != Vector2.Zero)
                        {
                            objectCollided = true;
                            // Detach alien object if we collide with tiles..
                            DetachObject(sourceObject);

                            if (objectParameters.PreviousBounds.Bottom <= tileBounds.Top)
                            {
                                objectParameters.IsOnGround = true;
                                objectParameters.IsJumping = false;
                            }

                            if (objectParameters.PreviousBounds.Right <= tileBounds.Left && objectParameters.Velocity.X > 0)
                            {
                                objectParameters.Velocity.X = 0;
                                objectParameters.Position = new Vector2(objectParameters.Position.X + depth.X, objectParameters.Position.Y);
                                bounds = AbsoluteCollisionRect(sourceObject);
                                // Recheck bounds...
                                x--;
                                continue;
                            }

                            if (objectParameters.PreviousBounds.Left >= tileBounds.Right && objectParameters.Velocity.X < 0 && objectParameters.IsJumping)
                            {
                                objectParameters.Velocity.X = 0;
                                objectParameters.Position = new Vector2(objectParameters.Position.X + depth.X, objectParameters.Position.Y);
                                bounds = AbsoluteCollisionRect(sourceObject);
                                // Recheck bounds...
                                x--;
                                continue;
                            }

                            if (objectParameters.PreviousBounds.Bottom <= tileBounds.Top || objectParameters.PreviousBounds.Top >= tileBounds.Bottom)
                            {
                                objectParameters.Position = new Vector2(objectParameters.Position.X, objectParameters.Position.Y + depth.Y);
                                bounds = AbsoluteCollisionRect(sourceObject);
                                if (objectParameters.Velocity.Y > 0)
                                {
                                    objectParameters.Position.Y = tileBounds.Top;
                                    objectParameters.IsOnGround = true;
                                    objectParameters.IsJumping = false;
                                }
                                objectParameters.Velocity.Y = 0;


                                // Recheck bounds...
                                x--;
                                continue;
                            }

                            objectParameters.Velocity.X = 0;
                            objectParameters.Position = new Vector2(objectParameters.Position.X + depth.X, objectParameters.Position.Y);
                            bounds = AbsoluteCollisionRect(sourceObject);
                        }
                    }
                }
            }
            leftTile = (int)Math.Floor((float)bounds.Left / Tile.Width);
            rightTile = (int)Math.Ceiling(((float)bounds.Right / Tile.Width)) - 1;
            topTile = (int)Math.Floor((float)bounds.Top / Tile.Height);
            bottomTile = (int)Math.Ceiling(((float)bounds.Bottom / Tile.Height)) - 1;
            if (objectParameters.IsOnGround == true &&
                level.GetCollision(leftTile, bottomTile + 1) != TileCollision.Impassable &&
                level.GetCollision(rightTile, bottomTile + 1) != TileCollision.Impassable)
            {
                objectParameters.IsOnGround = false;
            }


            // If the collision stopped us from moving, reset the velocity to zero.
            if (objectParameters.Position.X == objectParameters.PreviousPosition.X)
            {
                objectParameters.Velocity.X = 0;
            }
            if (objectParameters.Position.Y == objectParameters.PreviousPosition.Y)
            {
                objectParameters.Velocity.Y = 0;
            }
            return objectCollided;
        }

        internal void SetObjectPosition(GameObject sourceObject, Vector2 position)
        {
            PhysicsParameters objectParameters = sourceObject.Parameters;
            objectParameters.Position = position;
            objectParameters.Velocity = Vector2.Zero;
        }

        internal void StandOnAlien(GameObject sourceObject, GameObject alienObject)
        {
            PhysicsParameters objectParameters = sourceObject.Parameters;
            sourceObject.Alien = alienObject;
            objectParameters.IsJumping = false;
            objectParameters.IsOnGround = true;
            objectParameters.Velocity.Y = 0;
            objectParameters.Position.Y = alienObject.Parameters.Position.Y - alienObject.Parameters.SpriteSize.Height;
        }

        internal void DetachObject(GameObject sourceObject)
        {
            if (sourceObject.Alien != null)
            {
                sourceObject.Alien = null;
                sourceObject.Parameters.IsOnGround = false;
            }
        }

        internal void MoveLeft(GameObject sourceObject)
        {
            sourceObject.Parameters.Movement = -1.0f;
            sourceObject.Parameters.FaceDirection = -1.0f;
        }

        internal void MoveRight(GameObject sourceObject)
        {
            sourceObject.Parameters.Movement = 1.0f;
            sourceObject.Parameters.FaceDirection = 1.0f;
        }

        internal void Jump(GameObject sourceObject)
        {
            PhysicsParameters objectParameters = sourceObject.Parameters;
            if (objectParameters.IsOnGround || sourceObject.Alien != null)
            {
                DetachObject(sourceObject);
                objectParameters.IsJumping = true;
                objectParameters.IsOnGround = false;
                objectParameters.Velocity.Y = sourceObject.Parameters.JumpVelocity;

            }
        }
    }
}
